
// Copyright (C) 2012 Luca Piccioni
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//  
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//  
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Xml;

using Derm.Render;

namespace Derm.Xml.Collada
{
	/// <summary>
	/// The accessor element declares an access pattern to one of the array elements: float_array, 
	/// tnt_array, Name_array, bool_array, and IDREF_array. The accessor element describes access 
	/// to arrays that are organized in either an interleaved or non-interleaved manner, depending 
	/// on the offset and stride attributes.
	/// </summary>
	public class ColladaAccessor : ColladaXmlSerializable
	{
		#region Constructors

		/// <summary>
		/// Construct a ColladaAccessor.
		/// </summary>
		/// <param name="parent">
		/// The parent element containing this ColladaAccessor.
		/// </param>
		public ColladaAccessor(ColladaXmlSerializable parent) : base(parent)
		{
			
		}

		#endregion

		#region Accessor Information

		/// <summary>
		/// The count attribute indicates the number of times the array is accessed. Required attribute.
		/// </summary>
		public uint Count;

		/// <summary>
		/// The offset attribute indicates the index of the first value to be read from the array. The default value is 0. Optional attribute.
		/// </summary>
		public uint Offset;

		/// <summary>
		/// The source attribute indicates the location of the array to access using a URL expression. Required attribute.
		/// </summary>
		public string Source;

		/// <summary>
		/// The stride attribute indicates number of values to be considered a unit during each access to the array. The default
		/// value is 1, indicating that a single value is accessed. Optional attribute.
		/// </summary>
		public uint Stride = 1;

		/// <summary>
		/// The accessor element may have any number of param elements.
		/// </summary>
		public readonly List<ColladaParam> Params = new List<ColladaParam>();

		#endregion

		#region Data Flow Utilities

		/// <summary>
		/// Get an array buffer object representing the array accessed by this accessor.
		/// </summary>
		/// <returns>
		/// It returns an array buffer object representing the array accessed by this accessor. The
		/// 
		/// </returns>
		internal ArrayBufferObject GetAccessorArray()
		{
			ColladaDocument colladaDocument = GetColladaDocument();
			ColladaArray colladaArray = colladaDocument.GetSpecialElement<ColladaArray>(Source);

			if (Params.Count == 0)
				throw new InvalidOperationException("no accessor parameters");
			if (IsSimpleParamsConfiguration() == false)
				throw new NotSupportedException("complex accessor params not supported");

			string simpleParamsType = Params[0].Type;

			if (simpleParamsType == ArrayTypeFloat) {
				VertexArrayType arrayType = ArrayBufferItem.GetArrayType(VertexBaseType.Float, (uint)Params.Count);
				return (CreateAccessorArray<float>(colladaArray).Convert(arrayType));
			} else if (simpleParamsType == ArrayTypeInt) {
				return (CreateAccessorArray<int>(colladaArray));
			} else
				throw new NotSupportedException(String.Format("simple accessor params type {0} not supported", simpleParamsType));
		}

		/// <summary>
		/// Check whether all parameters have the same base type.
		/// </summary>
		/// <returns>
		/// It returns true whether all parameters defined in this accessor have the same base type.
		/// </returns>
		private bool IsSimpleParamsConfiguration()
		{
			foreach (ColladaParam param in Params)
				if (param.Type != Params[0].Type)
					return (false);

			return (true);
		}

		/// <summary>
		/// Count the used parameters.
		/// </summary>
		/// <returns>
		/// It returns the count of the used parameters. Not used parameters are not copied out from the array, but
		/// they are read and skipped.
		/// </returns>
		private uint GetUsedParamsCount()
		{
			uint count = 0;

			Params.ForEach(delegate(ColladaParam param) { if (String.IsNullOrEmpty(param.Name) == false) count++; });

			return (count);
		}

		/// <summary>
		/// Utility routine for creating accessor array of a certain type.
		/// </summary>
		/// <typeparam name="T"></typeparam>
		/// <param name="colladaArray"></param>
		/// <returns></returns>
		private ArrayBufferObject CreateAccessorArray<T>(ColladaArray colladaArray) where T : struct 
		{
			uint cursorBufferOffset = 0;

			ArrayBufferObject<T> arrayBufferObject = new ArrayBufferObject<T>(BufferObject.Hint.StaticCpuDraw);

			arrayBufferObject.Define(Count * GetUsedParamsCount());
			for (uint i = 0; i < Count; i++) {

				uint cursorSrcOffset = i * Stride + Offset;

				foreach (ColladaParam param in Params) {
					if (String.IsNullOrEmpty(param.Name) == false)
						arrayBufferObject[cursorBufferOffset++] = (T) Convert.ChangeType(colladaArray[cursorSrcOffset % colladaArray.Count], typeof(T)) ;
					cursorSrcOffset++;
				}
			}

			return (arrayBufferObject);
		}

		private const string ArrayTypeInt = "int";

		private const string ArrayTypeFloat = "float";

		private const string ArrayTypeName = "Name";

		private const string ArrayTypeBool = "bool";

		private const string ArrayTypeIdRef = "IDREF";

		#endregion

		#region ColladaXmlSerializable Overrides

		/// <summary>
		/// Read the attribute of the current XML serializable.
		/// </summary>
		/// <param name="xmlReader">
		/// A <see cref="XmlReader"/> used for reading XML element attributes.
		/// </param>
		/// <remarks>
		/// <para>
		/// This method is called only in the case there are effectively attributes on XML element. Don't expect that this
		/// method will be called even when the attributes count is zero. This is very important, since the design doesn't
		/// allow object initialization using this method.
		/// </para>
		/// </remarks>
		public override void ReadAttributes(XmlReader xmlReader)
		{
			if (xmlReader == null)
				throw new ArgumentNullException("xmlReader");

			string attribute;

			if ((attribute = xmlReader[ColladaAttributeCount]) == null)
				throw new InvalidOperationException("required attribute " + ColladaAttributeCount);
			Count = UInt32.Parse(attribute);

			if ((attribute = xmlReader[ColladaAttributeOffset]) != null)
				Offset = UInt32.Parse(attribute);

			if ((attribute = xmlReader[ColladaAttributeSource]) == null)
				throw new InvalidOperationException("required attribute " + ColladaAttributeSource);
			Source = attribute;

			if ((attribute = xmlReader[ColladaAttributStride]) != null)
				Stride = UInt32.Parse(attribute);
		}

		/// <summary>
		/// Read an element of the current XML serializable.
		/// </summary>
		/// <param name="elementName">
		/// A <see cref="System.String"/> that specify the element name.
		/// </param>
		/// <param name="xmlReader">
		/// A <see cref="XmlReader"/> used for reading child element. It is positioned at the start element.
		/// </param>
		/// <remarks>
		/// <para>
		/// This method is responsible to read the start element of the nested element.
		/// </para>
		/// <para>
		/// This method is resposible to read all children elements till the corresponding end element, without reading it. In the
		/// case this condition is not satisfied, the XML parsing will be aborted.
		/// </para>
		/// </remarks>
		public override void ReadElement(string elementName, XmlReader xmlReader)
		{
			if (elementName == null)
				throw new ArgumentNullException("elementName");
			if (xmlReader == null)
				throw new ArgumentNullException("xmlReader");

			if (elementName == ColladaElementParam) {
				ColladaParam param = new ColladaParam(this);
				param.ReadXml(xmlReader);
				Params.Add(param);
			} else
				XmlHelper.SkipXmlElement(xmlReader);
		}

		/// <summary>
		/// Collada 'accessor' attribute name (1.4).
		/// </summary>
		private const string ColladaAttributeCount = "count";

		/// <summary>
		/// Collada 'accessor' attribute name (1.4).
		/// </summary>
		private const string ColladaAttributeOffset = "offset";

		/// <summary>
		/// Collada 'accessor' attribute name (1.4).
		/// </summary>
		private const string ColladaAttributeSource = "source";

		/// <summary>
		/// Collada 'accessor' attribute name (1.4).
		/// </summary>
		private const string ColladaAttributStride = "stride";

		/// <summary>
		/// Collada 'library_images' child element name (1.4).
		/// </summary>
		private const string ColladaElementParam = "param";

		#endregion
	}

	/// <summary>
	/// The param element declares parametric information regarding its parent element.
	/// </summary>
	public class ColladaParam : ColladaXmlSerializable
	{
		#region Constructors

		/// <summary>
		/// Construct a ColladaParam.
		/// </summary>
		/// <param name="parent">
		/// The parent element containing this ColladaParam.
		/// </param>
		public ColladaParam(ColladaXmlSerializable parent) : base(parent)
		{
			
		}

		#endregion

		#region Param Information

		/// <summary>
		/// The parameter value.
		/// </summary>
		public string Value;

		/// <summary>
		/// The name attribute is the text string name of this element. Optional attribute.
		/// </summary>
		public string Name;

		/// <summary>
		/// The sid attribute is a text string value containing the sub-identifier of this element. 
		/// This value must be unique within the scope of the parent element. Optional attribute.
		/// </summary>
		public string Sid;

		/// <summary>
		/// The semantic attribute is the user-defined meaning of the parameter. Optional attribute.
		/// </summary>
		public string Semantic;

		/// <summary>
		/// The type attribute indicates the type of the value data. This text string must be understood
		/// by the application. Required attribute.
		/// </summary>
		public string Type;

		#endregion

		#region ColladaXmlSerializable Overrides

		/// <summary>
		/// Read the attribute of the current XML serializable.
		/// </summary>
		/// <param name="xmlReader">
		/// A <see cref="XmlReader"/> used for reading XML element attributes.
		/// </param>
		/// <remarks>
		/// <para>
		/// This method is called only in the case there are effectively attributes on XML element. Don't expect that this
		/// method will be called even when the attributes count is zero. This is very important, since the design doesn't
		/// allow object initialization using this method.
		/// </para>
		/// </remarks>
		public override void ReadAttributes(XmlReader xmlReader)
		{
			if (xmlReader == null)
				throw new ArgumentNullException("xmlReader");

			string attribute;

			if ((attribute = xmlReader[ColladaAttributeName]) != null)
				Name = attribute;

			if ((attribute = xmlReader[ColladaAttributeSid]) != null)
				Sid = attribute;

			if ((attribute = xmlReader[ColladaAttributeSemantic]) != null)
				Semantic = attribute;

			if ((attribute = xmlReader[ColladaAttributType]) == null)
				throw new InvalidOperationException("required attribute " + ColladaAttributType);
			Type = attribute;
		}

		/// <summary>
		/// Read the element text of the current XML serializable.
		/// </summary>
		/// <param name="xmlReader">
		/// A <see cref="XmlReader"/> used for reading child element. It is positioned at the start element.
		/// </param>
		public override void ReadText(XmlReader xmlReader)
		{
			if (xmlReader == null)
				throw new ArgumentNullException("xmlReader");

			Value = xmlReader.Value;
		}

		/// <summary>
		/// Collada 'param' attribute name (1.4).
		/// </summary>
		private const string ColladaAttributeName = "name";

		/// <summary>
		/// Collada 'param' attribute name (1.4).
		/// </summary>
		private const string ColladaAttributeSid = "sid";

		/// <summary>
		/// Collada 'param' attribute name (1.4).
		/// </summary>
		private const string ColladaAttributeSemantic = "semantic";

		/// <summary>
		/// Collada 'param' attribute name (1.4).
		/// </summary>
		private const string ColladaAttributType = "type";

		#endregion
	}

	/// <summary>
	/// The source element declares a data repository that provides values according to the semantics of an 
	/// input element that refers to it.
	/// </summary>
	public class ColladaSource : ColladaXmlSerializable
	{
		#region Constructors

		/// <summary>
		/// Construct a ColladaSource.
		/// </summary>
		/// <param name="parent">
		/// The parent element containing this ColladaSource.
		/// </param>
		public ColladaSource(ColladaXmlSerializable parent) : base(parent)
		{
			
		}

		/// <summary>
		/// Construct a ColladaSource.
		/// </summary>
		/// <param name="parent">
		/// The parent element containing this ColladaSource.
		/// </param>
		/// <param name="id"></param>
		public ColladaSource(ColladaXmlSerializable parent, string id) : base(parent)
		{
			Id = id;
		}

		#endregion

		#region Source Information

		/// <summary>
		/// The id attribute is a text string containing the unique identifier of this element.
		/// This value must be unique within the instance document. Required attribute.
		/// </summary>
		public string Id
		{
			get { return (mId); }
			set
			{
				if (value == null)
					throw new InvalidOperationException("required attribute");
				mId = value;
			}
		}

		private string mId;

		/// <summary>
		/// The name attribute is the text string name of this element. Optional attribute.
		/// </summary>
		public string Name;

		/// <summary>
		/// The source element may contain an asset element.
		/// </summary>
		public ColladaAsset Asset;

		/// <summary>
		/// The source element may contain an array.
		/// </summary>
		public ColladaArray Array;

		/// <summary>
		/// The technique common specifies the common method for accessing this source element's data.
		/// </summary>
		public ColladaMeshCommonTechnique CommonTechnique;

		/// <summary>
		/// This element may contain any number of non-common profile techniques.
		/// </summary>
		public readonly List<ColladaTechnique> Techniques = new List<ColladaTechnique>();

		#endregion

		#region Data Flow Utilities

		/// <summary>
		/// Define an <see cref="ArrayBufferObject"/> representing this source data.
		/// </summary>
		/// <returns>
		/// It returns an <see cref="ArrayBufferObject"/> representing this source data.
		/// </returns>
		internal ArrayBufferObject GetSourceArray()
		{
			if (CommonTechnique != null) {
				return (CommonTechnique.Accessor.GetAccessorArray());
			} else
				throw new NotSupportedException("no technique for accessing source data");
		}

		#endregion

		#region XmlSerializerExt Overrides

		/// <summary>
		/// Read the attribute of the current XML serializable.
		/// </summary>
		/// <param name="xmlReader">
		/// A <see cref="XmlReader"/> used for reading XML element attributes.
		/// </param>
		/// <remarks>
		/// <para>
		/// This method is called only in the case there are effectively attributes on XML element. Don't expect that this
		/// method will be called even when the attributes count is zero. This is very important, since the design doesn't
		/// allow object initialization using this method.
		/// </para>
		/// </remarks>
		public override void ReadAttributes(XmlReader xmlReader)
		{
			if (xmlReader == null)
				throw new ArgumentNullException("xmlReader");

			string attribute;

			if ((attribute = xmlReader[ColladaAttributeId]) == null)
				throw new InvalidOperationException("required attribute " + ColladaAttributeId);
			Id = attribute;

			if ((attribute = xmlReader[ColladaAttributeName]) != null)
				Name = attribute;
		}

		/// <summary>
		/// Read an element of the current XML serializable.
		/// </summary>
		/// <param name="elementName">
		/// A <see cref="System.String"/> that specify the element name.
		/// </param>
		/// <param name="xmlReader">
		/// A <see cref="XmlReader"/> used for reading child element. It is positioned at the start element.
		/// </param>
		/// <remarks>
		/// <para>
		/// This method is responsible to read the start element of the nested element.
		/// </para>
		/// <para>
		/// This method is resposible to read all children elements till the corresponding end element, without reading it. In the
		/// case this condition is not satisfied, the XML parsing will be aborted.
		/// </para>
		/// </remarks>
		public override void ReadElement(string elementName, XmlReader xmlReader)
		{
			if (elementName == null)
				throw new ArgumentNullException("elementName");
			if (xmlReader == null)
				throw new ArgumentNullException("xmlReader");

			if (elementName == ColladaElementAsset) {
				Asset = new ColladaAsset(this);
				Asset.ReadXml(xmlReader);
			} else if (elementName == ColladaElementIdRefArray) {
				Array = new ColladaIdRefArray(this);
				Array.ReadXml(xmlReader);
			} else if (elementName == ColladaElementNameArray) {
				Array = new ColladaNameArray(this);
				Array.ReadXml(xmlReader);
			} else if (elementName == ColladaElementBoolArray) {
				Array = new ColladaBoolArray(this);
				Array.ReadXml(xmlReader);
			} else if (elementName == ColladaElementFloatArray) {
				Array = new ColladaFloatArray(this);
				Array.ReadXml(xmlReader);
			} else if (elementName == ColladaElementIntArray) {
				Array = new ColladaIntArray(this);
				Array.ReadXml(xmlReader);
			} else if (elementName == ColladaElementTechnique) {
				Techniques.Add(ColladaTechnique.ReadTechique(xmlReader, this));
			} else if (elementName == ColladaElementCommonTechnique) {
				CommonTechnique = new ColladaMeshCommonTechnique(this);
				CommonTechnique.ReadXml(xmlReader);
			} else
				base.ReadElement(elementName, xmlReader);
		}

		/// <summary>
		/// Collada 'source' attribute name (1.4).
		/// </summary>
		private const string ColladaAttributeId = "id";

		/// <summary>
		/// Collada 'source' attribute name (1.4).
		/// </summary>
		private const string ColladaAttributeName = "name";

		/// <summary>
		/// Collada 'source' child element name (1.4).
		/// </summary>
		private const string ColladaElementAsset = "asset";

		/// <summary>
		/// Collada 'source' child element name (1.4).
		/// </summary>
		private const string ColladaElementIdRefArray = "IDREF_array";

		/// <summary>
		/// Collada 'source' child element name (1.4).
		/// </summary>
		private const string ColladaElementNameArray = "Name_array";

		/// <summary>
		/// Collada 'source' child element name (1.4).
		/// </summary>
		private const string ColladaElementBoolArray = "bool_array";

		/// <summary>
		/// Collada 'source' child element name (1.4).
		/// </summary>
		private const string ColladaElementFloatArray = "float_array";

		/// <summary>
		/// Collada 'source' child element name (1.4).
		/// </summary>
		private const string ColladaElementIntArray = "int_array";

		/// <summary>
		/// Collada 'source' child element name (1.4).
		/// </summary>
		private const string ColladaElementCommonTechnique = "technique_common";

		/// <summary>
		/// Collada 'source' child element name (1.4).
		/// </summary>
		private const string ColladaElementTechnique = "technique";

		#endregion
	}
}
